// Author: wsfuyibing <682805@qq.com>
// Date: 2025-04-11

package gen_model

import (
	"context"
	_ "embed"
	"fmt"
	"gitee.com/go-libs/console"
	"gitee.com/go-libs/log"
	"gitee.com/go-wares/framework-gin/framework/commands"
	"gitee.com/go-wares/framework-gin/framework/util/database"
	"gorm.io/gorm"
	"regexp"
	"sort"
	"strings"
)

const (
	cmdDesc = "Generate model and service files"
	cmdName = "gen:model"
)

var cmdOptions = map[string]*commands.Option{
	"dsn": {
		Description:  "Database source name, Configured in config/db.yaml",
		DefaultValue: "db",
		Kind:         console.KindString,
		Required:     false,
		Short:        'd',
	},
	"model": {
		Description: "Model name, it's a table real name",
		Kind:        console.KindString,
		Required:    true,
		Short:       'm',
	},
	"override": {
		Description: "Replace if source file is exists",
		Kind:        console.KindNull,
		Short:       'o',
	},
	"target": {
		Description:  "Generated files save location",
		DefaultValue: "/app",
		Kind:         console.KindString,
		Required:     false,
		Short:        't',
	},
}

//go:embed templates/model.tpl
var tplModel string

//go:embed templates/service.tpl
var tplService string

type Provider struct {
	cmd    *console.Command
	common commands.Provider

	dsn      string
	model    string
	override bool
	target   string
}

func (o *Provider) Before(_ context.Context, container *console.Container, cmd *console.Command) (err error) {
	var target = "/app"

	log.Config().SetLevel("fatal")

	// Common: working dir and module name, module path.
	if err = o.common.InitModule(); err != nil {
		return
	}

	// Scopes: target dir for built files.
	if opt, has := cmd.GetOption("target"); has {
		if str := opt.ToString(); str != "" {
			target = regexp.MustCompile(`/+`).ReplaceAllString(fmt.Sprintf(`/%s`, str), "/")
		}
	}
	o.target = fmt.Sprintf(`%s%s`, o.common.WorkingDir, target)

	// Scopes: database source name.
	o.dsn = "db"
	if opt, has := cmd.GetOption("dsn"); has {
		if str := opt.ToString(); str != "" {
			o.dsn = str
		}
	}

	// Scopes: model name.
	if opt, has := cmd.GetOption("model"); has {
		if str := opt.ToString(); str != "" {
			o.model = str
		}
	}

	// Scopes: override.
	if opt, has := cmd.GetOption("override"); has {
		o.override = opt.GetSpecified()
	}

	// Start to generate.
	container.GetOutput().Info(cmd.GetDescription())
	container.GetOutput().Info(`  mod name: %s`, o.common.ModuleName)
	container.GetOutput().Info(`  mod path: %s`, o.common.ModulePath)
	container.GetOutput().Info(`  work dir: %s`, o.common.WorkingDir)
	container.GetOutput().Info(`  target  : %s`, o.target)
	return
}

func (o *Provider) Run(ctx context.Context, container *console.Container, cmd *console.Command) (err error) {
	var (
		db           *gorm.DB
		list, tables []*Table
	)

	// List table and columns.
	if db, err = database.Mgr.GetMasterConnection(ctx, o.dsn); err == nil {
		if tables, err = o.listTables(db); err == nil {
			list = make([]*Table, 0)
			for _, table := range tables {
				if table.Name.Match(o.model) {
					if err = o.listColumns(db, table); err != nil {
						break
					}
					list = append(list, table)
				}
			}
		}
	}
	if err != nil {
		return
	}

	// Generate files.
	for _, table := range list {
		container.GetOutput().Info(`Generate from table: %s`, table.Name)

		// Generate model.
		if err = o.generateModel(container, table); err != nil {
			break
		}
		// Generate service.
		if err = o.generateService(container, table); err != nil {
			break
		}
	}
	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Provider) generateColumns(table *Table) (pk string, mapper map[string]string, packages []string) {
	var (
		pkg       string // gitee.com/go-wares/framework-gin/framework
		pkgMapper = make(map[string]string)
		size      int
		typ       string // framework.MysqlDatetime
	)

	// Sortable mapper.
	//
	// {
	//   "0004_Id": "Id int32 `gorm:"column:id,primaryKey" json:"id"`"
	// }
	mapper = make(map[string]string)

	// Import packages.
	//
	// [
	//   "gitee.com/go-wares/framework-gin/framework"
	// ]
	packages = make([]string, 0)

	// Range columns of table.
	for _, column := range table.Columns {
		if column == nil {
			continue
		}

		// Need to import relationship package.
		if size, typ, pkg = column.Type.Export(); pkg != "" {
			if _, ok := pkgMapper[pkg]; !ok {
				pkgMapper[pkg] = typ
				packages = append(packages, pkg)
			}
		}

		if pk == "" && column.Key.IsPrimary() {
			pk = column.Field.String()
		}

		camel := column.Field.Camel()
		orm := column.Type.ParseOrm(table, column)
		snake := column.Field.Snake()
		key := fmt.Sprintf(`%04d%s`, size, camel)

		mapper[key] = "\n    " + fmt.Sprintf("%s %s `gorm:\"%s\" json:\"%s\"` // align=%v",
			camel, typ, orm, snake, size,
		)
	}
	return
}

func (o *Provider) generateModel(container *console.Container, table *Table) (err error) {
	var (
		ignored bool
		src     = fmt.Sprintf(`%s/models/%s`, o.target, table.Name.Filename("model"))
	)

	// Ignore to do if file existed and override is disabled.
	if ignored, err = o.common.CheckOverride(src, o.override); err != nil || ignored {
		container.GetOutput().Info(`  [ignored] %s`, strings.TrimPrefix(src, o.common.WorkingDir))
		return
	}

	// Generate data.
	pk, mapper, packages := o.generateColumns(table)
	data := o.common.InitTemplateData(o.cmd)
	data["Dsn"] = o.dsn
	data["ModelName"] = table.Name.StructName()
	data["TableName"] = table.Name
	data["PrimaryKeyName"] = pk

	// Package list.
	if len(packages) > 0 {
		sort.Strings(packages)
		str := "import ("
		for _, pkg := range packages {
			str += "\n    " + fmt.Sprintf(`"%s"`, pkg)
		}
		str += "\n)"
		data["Packages"] = str
	}

	// Column list.
	cs := ""
	ls := make([]string, 0)
	for k, _ := range mapper {
		ls = append(ls, k)
	}
	sort.Strings(ls)
	for _, k := range ls {
		if v, ok := mapper[k]; ok {
			cs += v
		}
	}
	data["ColumnList"] = cs

	// Save generated file.
	if err = o.common.SaveFile(src, tplModel, data); err == nil {
		container.GetOutput().Info(`  [success] %s`, strings.TrimPrefix(src, o.common.WorkingDir))
	}
	return
}

func (o *Provider) generateService(container *console.Container, table *Table) (err error) {
	var (
		data    map[string]any
		ignored bool
		src     = fmt.Sprintf(`%s/services/%s`, o.target, table.Name.Filename("service"))
	)

	if ignored, err = o.common.CheckOverride(src, o.override); err != nil || ignored {
		container.GetOutput().Info(`  [ignored] %s`, strings.TrimPrefix(src, o.common.WorkingDir))
		return
	}

	// Generate data.
	data = o.common.InitTemplateData(o.cmd)
	data["Dsn"] = o.dsn
	data["ModelName"] = table.Name.StructName()
	data["TableName"] = table.Name
	data["TargetPath"] = strings.ReplaceAll(o.target, o.common.WorkingDir, o.common.ModuleName)

	if err = o.common.SaveFile(src, tplService, data); err == nil {
		container.GetOutput().Info(`  [success] %s`, strings.TrimPrefix(src, o.common.WorkingDir))
	}
	return
}

func (o *Provider) init() *Provider {
	o.cmd = console.NewCommand(cmdName).SetDescription(cmdDesc).SetProvider(o)
	o.initOptions()
	return o
}

func (o *Provider) initOptions() {
	for name, option := range cmdOptions {
		if option == nil {
			return
		}
		opt := console.NewOption(name).
			SetDescription(option.Description).
			SetKind(option.Kind).
			SetRequired(option.Required)
		if option.Short > 0 {
			opt.SetShortName(option.Short)
		}
		if option.DefaultValue != nil {
			opt.SetDefaultValue(option.DefaultValue)
		}
		o.cmd.Add(opt)
	}
}

func (o *Provider) listColumns(db *gorm.DB, table *Table) (err error) {
	table.Columns = make([]*Column, 0)
	err = db.Raw(fmt.Sprintf("SHOW FULL COLUMNS FROM `%s`", table.Name)).Scan(&table.Columns).Error
	return
}

func (o *Provider) listTables(db *gorm.DB) (tables []*Table, err error) {
	tables = make([]*Table, 0)
	err = db.Raw("SHOW TABLE STATUS").Scan(&tables).Error
	return
}

func New() *console.Command {
	return (&Provider{}).init().cmd
}
